package com.spring.nio.service.file.util;

import com.alibaba.fastjson.JSON;
import com.spring.nio.business.file.service.FileService;
import com.spring.nio.business.user.dto.UserDTO;
import com.spring.nio.business.user.param.UserQueryParam;
import com.spring.nio.business.user.service.UserService;
import com.spring.nio.common.BasicConstant;
import com.spring.nio.core.channel.context.ChannelContext;
import com.spring.nio.core.channel.context.SimpleChannelContext;
import com.spring.nio.core.channel.context.SocketChannelContext;
import com.spring.nio.core.channel.handler.AbstractChannelHandler;
import com.spring.nio.core.configuration.NioServerBootstrap;
import com.spring.nio.core.event.handler.AbstractEventHandler;
import com.spring.nio.core.event.handler.WriteEventHandler;
import com.spring.nio.core.server.FileSystemServer;
import com.spring.nio.core.startup.BasicServer;
import com.spring.nio.service.chat.frame.ChatMessageFrame;
import com.spring.nio.service.file.frame.FileMessageFrame;
import com.spring.nio.service.file.model.FileEndTransportRequest;
import com.spring.nio.service.file.model.FileOperateEnum;
import com.spring.nio.service.file.model.FileTransportRequest;
import com.spring.nio.service.file.model.structure.FileTransportTaskDTO;
import com.spring.nio.service.protocol.response.file.FileTransportResponse;
import com.spring.nio.service.protocol.response.file.TransportResponse;
import com.spring.nio.util.FileUtil;
import com.spring.nio.util.LocalTime;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 文件传输管理器
 * @author spring
 * */
@Slf4j
public class FileTransportManager {
    public static String basePath = "";
    private static UserService userService = null;
    private static FileService fileService = null;
    public static final Map<String, FileTransportTaskDTO> temporaryTransportMap = new ConcurrentHashMap<>();
    static {
        if(StringUtils.contains(BasicServer.getMap().get(BasicConstant.OS_NAME).toString(), "Win")) {
            basePath = BasicServer.getMap().get(BasicConstant.NIO_FILE_BASE_PATH_WINDOWS).toString();
        } else {
            basePath = BasicServer.getMap().get(BasicConstant.NIO_FILE_BASE_PATH_LINUX_MAC).toString();
        }

        ClassPathXmlApplicationContext classPathXmlApplicationContext = NioServerBootstrap.getConfig().getClassPathXmlApplicationContext();
        userService = classPathXmlApplicationContext.getBean(UserService.class);
        fileService = classPathXmlApplicationContext.getBean(FileService.class);
    }

    /**
     * 文件在线传输或是网盘上传处理
     * @param fileMessageFrame
     * @param socketChannelContext
     * @param channelContext
     */
    public static void executeFileTransportHandler(FileMessageFrame fileMessageFrame, SocketChannelContext socketChannelContext, ChannelContext channelContext) throws IOException {
        // 1、解析文件在线传输帧
        FileTransportRequest fileTransportRequest = JSON.parseObject(fileMessageFrame.getData(), FileTransportRequest.class);
        fileMessageFrame.setFileTransport(fileTransportRequest);

        // 2、文件在线传输
        if(fileMessageFrame.getFrameType().equals(FileMessageFrame.FrameType.ONLINE_TRANSPORT)) {
            if(fileMessageFrame.getFileOperateType().equals(FileMessageFrame.FileOperateType.SEND_TRANSPORT)) {
                fileSendOnlineTransportHandler(socketChannelContext, fileMessageFrame, channelContext);
            }

            if(fileMessageFrame.getFileOperateType().equals(FileMessageFrame.FileOperateType.RECEIVE_TRANSPORT)) {
                fileReceiveOnlineTransportHandler(socketChannelContext, fileMessageFrame, channelContext);
            }

            ((SimpleChannelContext) channelContext).setNeedStop(Boolean.TRUE);
        }

        // 3、文件上传
        if(fileMessageFrame.getFrameType().equals(FileMessageFrame.FrameType.UPLOAD)) {
            executeFileUploadHandler(socketChannelContext, fileMessageFrame, basePath);
            ((SimpleChannelContext) channelContext).setNeedStop(Boolean.TRUE);
        }

        // 4、文件下载
        if(fileMessageFrame.getFrameType().equals(FileMessageFrame.FrameType.DOWNLOAD)) {
            executeFileDownloadHandler(socketChannelContext, fileMessageFrame, channelContext);
            ((SimpleChannelContext) channelContext).setNeedSkip(Boolean.TRUE).setSkip(3);
        }

        // 5、文件流帧结束处理，此方法已过时，交由ChatRealDataHandler进行处理
        if(fileMessageFrame.getFrameType().equals(FileMessageFrame.FrameType.DATA_TRANSPORT_END)) {
            executeFileTransportEndHandler(fileMessageFrame, socketChannelContext, channelContext);
            // 设置当前ChannelContext为终止Handler
            ((SimpleChannelContext) channelContext).setNeedStop(Boolean.TRUE);
        }
    }

    /**
     * 文件在线传输发送端用户处理(为发起端用户设置关于文件聊天的FileSocketChannelContext)
     * @param socketChannelContext
     * @param fileMessageFrame
     * @param channelContext
     * */
    private static void fileSendOnlineTransportHandler(SocketChannelContext socketChannelContext, FileMessageFrame fileMessageFrame, ChannelContext channelContext) {
        // 1、查询接收用户: 判断是否在线，在线执行远程发送，不在线执行离线传输
        UserQueryParam userQueryParam = new UserQueryParam();
        userQueryParam.setUserId(fileMessageFrame.getFileTransport().getReceiveUserId());
        UserDTO receiveUserDto = userService.getOnlineUserById(userQueryParam);
        if(!Optional.ofNullable(receiveUserDto).isPresent()) {

            // 2.1.2、用户不在线，执行离线传输,跳过下一个ChannelContext,直接存储文件即可 --> 对应处理器为FileReceiveHandler
            ((SimpleChannelContext) channelContext).setNeedStop(Boolean.TRUE);
            return;
        }

        // 2、查询发起用户: 为发起端用户添加在线文件传输任务
        userQueryParam.setUserId(fileMessageFrame.getFileTransport().getLaunchUserId());
        UserDTO launchUserDto = userService.getOnlineUserById(userQueryParam);
        launchUserDto.setFileSocketChannelContext(socketChannelContext);
        if(CollectionUtils.isEmpty(launchUserDto.getUploadFileMap())) {
            launchUserDto.setUploadFileMap(new ConcurrentHashMap<>());
        }
        FileTransportTaskDTO fileTransportTaskDto = new FileTransportTaskDTO();
        fileTransportTaskDto.setFileTag(fileMessageFrame.getFileTransport().getTag());
        // 文件发送端用户对应的文件发送socketChannel
        fileTransportTaskDto.setSocketChannelContext(socketChannelContext);
        fileTransportTaskDto.setUserId(fileMessageFrame.getFileTransport().getLaunchUserId());
        fileTransportTaskDto.setUserName(fileMessageFrame.getFileTransport().getLaunchUserName());
        fileTransportTaskDto.setFileLength(Long.parseLong(fileMessageFrame.getFileLength().toString()));
        fileTransportTaskDto.setPacketIndex(0);
        fileTransportTaskDto.setSumSendBytes(0l);
        fileTransportTaskDto.setTaskStatus("INIT");
        launchUserDto.getUploadFileMap().putIfAbsent(fileTransportTaskDto.getFileTag(), fileTransportTaskDto);

        // 3、向远程接收用户发送接收文件的消息，由用户判断是否需要接收传送或是放弃；此处需要远程用户确定要接收，并且建立新的socketChannel连接用于接收文件
        FileTransportResponse transportResponse = new FileTransportResponse();
        transportResponse.setCode("200");
        transportResponse.setFrameType(ChatMessageFrame.FrameType.TEXT.getBit());
        transportResponse.setOperate(FileOperateEnum.ONLINE_TRANSPORT_NEED_CONFIRM.getOperate());
        transportResponse.setMessageType(FileOperateEnum.ONLINE_TRANSPORT_NEED_CONFIRM.getOperate());
        transportResponse.setMessage("待接收文件 [ " + fileMessageFrame.getFileTransport().getFileName() + " ]");
        transportResponse.setTag(fileMessageFrame.getFileTransport().getTag());
        transportResponse.setFileName(fileMessageFrame.getFileTransport().getFileName());
        transportResponse.setFileSize(Long.parseLong(fileMessageFrame.getFileLength().toString()));
        transportResponse.setLaunchUserName(launchUserDto.getUserName());
        transportResponse.setReceiveUserName(receiveUserDto.getUserName());
        transportResponse.setUserId(launchUserDto.getId());
        transportResponse.setStatus("1");
        transportResponse.setTime(Timestamp.valueOf(LocalDateTime.now()));
        log.info("[ " + LocalTime.formatDate(LocalDateTime.now()) + " ] FileTransportManager | --> 接收用户 = [{}] 接收到一条来自用户 = [{}] 发送的在线文件实时传输, 需要接收端用户确认是否接收", receiveUserDto.getUserName(), launchUserDto.getUserName());
        WriteEventHandler.addSendData(transportResponse, receiveUserDto.getChatSocketChannelContext());
    }

    /**
     * 文件在线传输接收端用户处理
     * @param socketChannelContext
     * @param fileMessageFrame
     * @param channelContext
     * */
    private static void fileReceiveOnlineTransportHandler(SocketChannelContext socketChannelContext, FileMessageFrame fileMessageFrame, ChannelContext channelContext) {
        // 1、判断是否是确认接收
        FileTransportRequest fileTransportRequest = fileMessageFrame.getFileTransport();
        if(!fileTransportRequest.getIsConfirmReceive()) {
            return;
        }

        // 2、查询接收端用户并为其构建文件在线接受任务
        UserQueryParam userQueryParam = new UserQueryParam();
        userQueryParam.setUserId(fileTransportRequest.getReceiveUserId());
        UserDTO receiveUserDto = userService.getOnlineUserById(userQueryParam);
        receiveUserDto.setFileSocketChannelContext(socketChannelContext);
        if(CollectionUtils.isEmpty(receiveUserDto.getDownloadFileMap())) {
            receiveUserDto.setDownloadFileMap(new ConcurrentHashMap<>());
        }
        FileTransportTaskDTO fileTransportTaskDto = new FileTransportTaskDTO();
        fileTransportTaskDto.setFileTag(fileTransportRequest.getTag());
        // 文件接收端用户对应socketChannel
        fileTransportTaskDto.setSocketChannelContext(socketChannelContext);
        fileTransportTaskDto.setUserId(fileTransportRequest.getReceiveUserId());
        fileTransportTaskDto.setUserName(fileTransportRequest.getReceiveUserName());
        fileTransportTaskDto.setFileLength(fileTransportRequest.getFileSize());
        fileTransportTaskDto.setPacketIndex(0);
        fileTransportTaskDto.setSumSendBytes(0L);
        fileTransportTaskDto.setTaskStatus("INIT");
        receiveUserDto.getDownloadFileMap().putIfAbsent(fileTransportTaskDto.getFileTag(), fileTransportTaskDto);

        // 3、向文件在线接收端 发送 [文件大小] 和 [下载循环次数], 等待接收文件传输流
        FileTransportResponse fileTransportResponse = new FileTransportResponse();
        fileTransportResponse.setCode("200");
        fileTransportResponse.setFrameType(FileMessageFrame.FrameType.DOWNLOAD.getBit());
        fileTransportResponse.setOperate(FileOperateEnum.DOWNLOAD_TRANSPORT_CONFIRM.getOperate());
        fileTransportResponse.setDownloadLoopCount(FileUtil.executeFileTransportLoopCount(fileTransportTaskDto.getFileLength()));
        fileTransportResponse.setFileSize(fileTransportTaskDto.getFileLength());
        fileTransportResponse.setTime(Timestamp.valueOf(LocalDateTime.now()));
        WriteEventHandler.addSendData(fileTransportResponse, receiveUserDto.getFileSocketChannelContext());
        log.info("[ " + LocalTime.formatDate(LocalDateTime.now()) + " ] FileTransportManager | --> 接收用户 = [{}] 同意开始接收文件在线传送", receiveUserDto.getUserName());

        // 4、向文件在线发起端 发送文件开始传输帧 告知文件发送端开始发送文件流
        TransportResponse transportResponse = new TransportResponse();
        transportResponse.setCode("200");
        transportResponse.setFrameType(FileMessageFrame.FrameType.ONLINE_TRANSPORT.getBit());
        transportResponse.setOperate(FileOperateEnum.ONLINE_TRANSPORT_NEED_CONFIRM.getOperate());
        transportResponse.setTime(Timestamp.valueOf(LocalDateTime.now()));
        userQueryParam.setUserId(fileMessageFrame.getFileTransport().getLaunchUserId());
        UserDTO launchUserDto = null;
        WriteEventHandler.addSendData(transportResponse, (launchUserDto = userService.getOnlineUserById(userQueryParam)).getFileSocketChannelContext());
        log.info("[ " + LocalTime.formatDate(LocalDateTime.now()) + " ] FileTransportManager | --> 接收用户 = [{}] 向发送用户 = [{}] 发送开始传送文件流响应帧", receiveUserDto.getUserName(), launchUserDto.getUserName());
    }

    /**
     * 网盘文件上传处理
     * @param socketChannelContext 文件上传客户端通道
     * @param fileMessageFrame
     * @param basePath 文件上传根路径
     */
    private static void executeFileUploadHandler(SocketChannelContext socketChannelContext, FileMessageFrame fileMessageFrame, String basePath) throws IOException {
        FileTransportRequest fileTransportRequest = fileMessageFrame.getFileTransport();

        // 1、待上传文件校验是否存在,如果存在禁止上传
        String filePath = fileTransportRequest.getNotLogin()? (fileTransportRequest.getFilePath() + fileTransportRequest.getFileName()) : (basePath + fileTransportRequest.getGroup() + fileTransportRequest.getFileName());
        if(FileSystemServer.isExist(filePath)){
            sendRepeatFileUploadMessage(fileTransportRequest, fileMessageFrame, socketChannelContext);
            return;
        }

        // 2、创建文件上传任务
        FileTransportTaskDTO fileTransportTaskDto = new FileTransportTaskDTO();
        fileTransportTaskDto.setCreateDate(new Date());
        fileTransportTaskDto.setSocketChannelContext(socketChannelContext);
        fileTransportTaskDto.setFileTag(fileTransportRequest.getTag());
        fileTransportTaskDto.setFile(FileSystemServer.createFile(filePath));
        fileTransportTaskDto.setFileLength(fileTransportRequest.getFileSize());
        fileTransportTaskDto.setFileChannel(FileChannel.open(fileTransportTaskDto.getFile().toPath(), StandardOpenOption.CREATE , StandardOpenOption.WRITE));
        fileTransportTaskDto.setIsFileChannelClose(Boolean.FALSE);
        fileTransportTaskDto.setIsFileDelete(Boolean.FALSE);
        fileTransportTaskDto.setSumSendBytes(0L);
        fileTransportTaskDto.setTaskStatus("INIT");
        fileTransportTaskDto.setRemark("暂无备注");

        // 3、根据请求状态判断当前用户是否登录，是则查询当前文件上传对应用户，并为用户userDto添加上传任务; 否则为临时传送文件
        UserQueryParam userQueryParam = new UserQueryParam();
        userQueryParam.setUserName(fileTransportRequest.getLaunchUserName());
        UserDTO userDto = null;
        if(fileTransportRequest.getNotLogin()) {
            // 用户未登录，即直接通过接口进行上传,查询当前上传用户是否存在
            userDto = userService.getUserByName(userQueryParam);
            if(Objects.isNull(userDto)) {
                log.info("[ " + LocalTime.formatDate(LocalDateTime.now()) + " ] FileTransportManager | --> 文件 [{}] 上传用户 [{}] 不存在, 上传失败", fileTransportRequest.getFileName(), fileTransportRequest.getLaunchUserName());
                return;
            }

            // 用户存在，但未登录，开启存在的用户在未登录状态下上传文件,使用临时文件上传集合
            temporaryTransportMap.putIfAbsent(socketChannelContext.getRemoteAddress(), fileTransportTaskDto);
        } else {
            // 用户已登录，按照登录用户方式上传
            userDto = userService.getOnlineUserByName(userQueryParam);
            if(CollectionUtils.isEmpty(userDto.getUploadFileMap())) { userDto.setUploadFileMap(new ConcurrentHashMap<>()); }
            userDto.getUploadFileMap().putIfAbsent(socketChannelContext.getRemoteAddress(), fileTransportTaskDto);
        }

        fileTransportTaskDto.setUserName(userDto.getUserName());
        fileTransportTaskDto.setUserId(userDto.getId());
        log.info("[ " + LocalTime.formatDate(LocalDateTime.now()) + " ] FileTransportManager | --> 用户 [{}] 待上传文件 [{}] 任务创建成功, 上传路径 = [{}]", userDto.getUserName(), fileTransportRequest.getFileName(), fileTransportTaskDto.getFile().toPath().toAbsolutePath());

        // 4、发送文件开始上传通知
        FileTransportResponse fileTransportResponse = new FileTransportResponse();
        fileTransportResponse.setCode("200");
        fileTransportResponse.setResponseName(BasicConstant.FILE_FRAME);
        fileTransportResponse.setFrameType(FileMessageFrame.FrameType.UPLOAD_TRANSPORT_CONFIRM.getBit());
        fileTransportResponse.setOperate(FileOperateEnum.UPLOAD_TRANSPORT_CONFIRM.getOperate());
        fileTransportResponse.setMessage("[ " + fileMessageFrame.getFileTransport().getFileName() + " ] 校验完成, 开始进行文件传输");
        fileTransportResponse.setFileTransportAddress(socketChannelContext.getRemoteAddress());
        fileTransportResponse.setTag(fileTransportRequest.getTag());
        fileTransportResponse.setLaunchUserName(userDto.getUserName());
        fileTransportResponse.setStatus("1");
        fileTransportResponse.setTime(Timestamp.valueOf(LocalDateTime.now()));
        log.info("[ " + LocalTime.formatDate(LocalDateTime.now()) + " ] FileTransportManager | --> 用户 [{}] 文件 [{}] 初始化完成，通知客户端开始发送文件数据", fileTransportRequest.getLaunchUserName(), fileTransportRequest.getFileName());
        WriteEventHandler.addSendData(fileTransportResponse, socketChannelContext);
    }

    /**
     * 文件下载处理
     * @param socketChannelContext 文件下载客户端通道
     * @param fileMessageFrame
     * @param channelContext
     * @return
     */
    @Deprecated
    public static void executeFileDownloadHandler(SocketChannelContext socketChannelContext, FileMessageFrame fileMessageFrame, ChannelContext channelContext) throws IOException {
        // 1、解析文件在线传输帧
        FileTransportRequest fileTransport = JSON.parseObject(fileMessageFrame.getData(), FileTransportRequest.class);
        fileMessageFrame.setFileTransport(fileTransport);
    }

    /**
     * 文件流帧结束处理
     * @param socketChannelContext 文件在线传输或是存储发起者对应的socketChannel通道附件
     * @param fileMessageFrame
     * @param channelContext
     * @return
     */
    public static void executeFileTransportEndHandler(FileMessageFrame fileMessageFrame, SocketChannelContext socketChannelContext, ChannelContext channelContext) {
        FileEndTransportRequest fileEndTransportRequest = JSON.parseObject(fileMessageFrame.getData(), FileEndTransportRequest.class);
        // 1、结束当前用户文件传送任务
        fileService.finishFileTransportTask(fileEndTransportRequest);
        // 2、由于文件流传送帧处理完成，需要释放当前通道对应的读取缓存数据
        AbstractEventHandler.clearChannelCache(socketChannelContext.getRemoteAddress());
    }

    /**
     * 发送重复上传文件的消息
     * @param fileTransportRequest
     * @param fileMessageFrame
     * @param socketChannelContext
     * */
    private static void sendRepeatFileUploadMessage(FileTransportRequest fileTransportRequest, FileMessageFrame fileMessageFrame, SocketChannelContext socketChannelContext) {
        log.info("[ " + LocalTime.formatDate(LocalDateTime.now()) + " ] FileTransportManager | --> 待上传文件 = [{}] 校验完成，当前目录下 = [{}] 已存在, 请修改文件名称后上传, thread = {}", fileTransportRequest.getFileName(), fileTransportRequest.getGroup(), Thread.currentThread().getName());

        TransportResponse transportResponse = new TransportResponse();
        transportResponse.setCode("200");
        transportResponse.setResponseName(BasicConstant.FILE_FRAME);
        transportResponse.setFrameType(FileMessageFrame.FrameType.UPLOAD_TRANSPORT_CONFIRM.getBit());
        transportResponse.setOperate(FileOperateEnum.UPLOAD_TRANSPORT_CONFIRM_REPEAT.getOperate());
        transportResponse.setMessage("[ " + fileMessageFrame.getFileTransport().getFileName() + " ] 校验完成, 当前目录下 [" + fileTransportRequest.getGroup() + "] 已存在, 请修改文件名称后上传");
        transportResponse.setStatus("1");
        transportResponse.setTime(Timestamp.valueOf(LocalDateTime.now()));
        WriteEventHandler.addSendData(transportResponse, socketChannelContext);
    }
}
